這篇我們使用 Python 與 Anthropic 官方 SDK,實作一個可以在終端機連續聊天的對話程式。我們先從最基本的一問一答開始,再處理讓程式記住前文的問題,讓模型能接續上一輪話題順暢回答。
完成後,程式會在終端機持續接收訊息,並保留前幾輪的對話內容。使用者提出後續問題時,不必重新說明前面的需求,模型也能根據對話歷史接續回答。
這系列範例使用 uv 管理 Python 專案與依賴。執行 uv sync 時,uv 會為專案建立獨立的虛擬環境,並依照鎖定的版本安裝套件。
💡 範例程式碼庫
本系列所有章節的完整範例程式碼皆收錄在 GitHub :evanchen76/ai-agent-sample。你可以先將專案 clone 到本地端進行操作。
這個對話程式可以使用 Anthropic 或 OpenAI 的模型。進入 chat-model-basics 範例目錄後執行 uv sync,專案會安裝兩家的官方 Python SDK。依照選用的服務設定其中一組 API Key,後續章節統一以 Anthropic Claude API 當作範例。
git clone https://github.com/evanchen76/ai-agent-sample.git
uv sync
# 使用 Anthropic
export ANTHROPIC_API_KEY="..."
# 使用 OpenAI
export OPENAI_API_KEY="..."
為了讓後續範例共用初始化邏輯,我們在 src/chat_model_basics/model.py 中封裝 Client 的建立與模型常數:
# src/chat_model_basics/model.py
import os
from anthropic import Anthropic
# 指定預設使用的 Claude 模型
MODEL_NAME = "claude-haiku-4-5"
def create_client() -> Anthropic:
if not os.getenv("ANTHROPIC_API_KEY"):
raise SystemExit("請先設定 ANTHROPIC_API_KEY")
return Anthropic()
def get_model_name() -> str:
return MODEL_NAME
這段初始化邏輯有兩個設計重點:
ANTHROPIC_API_KEY 讀取,避免寫死在程式碼中。MODEL_NAME 常數明確指定。這裡預設選用 claude-haiku-4-5;若後續任務需要更強的推理能力,也可以直接換成其他模型。我們先用最簡單的方式呼叫模型:透過 input() 讀取終端機的一行文字,將訊息發送給 Claude API,取得文字回覆後印出。在 src/chat_model_basics/ 新增 single_turn.py。先用 input() 從終端機取得使用者輸入,再把問題交給 ask_once():
from chat_model_basics.model import create_client, get_model_name
def main() -> None:
# 1. 初始化 Client 與取得模型名稱
client = create_client()
model = get_model_name()
# 2. 讀取終端機輸入
question = input("你:").strip()
if not question:
return
# 3. 發送請求並印出 AI 回覆
answer = ask_once(client, model, question)
print(f"AI:{answer}")
取得問題後,ask_once() 會包裝成一則 user 訊息傳送給模型:
def ask_once(client, model: str, question: str) -> str:
response = client.messages.create(
model=model,
max_tokens=1024,
messages=[{"role": "user", "content": question}],
)
return response_text(response)
這次請求使用三個參數:
model 指定要呼叫的 Claude 模型。max_tokens 限制這次回覆最多產生的 Token 數量。messages 是要交給模型的對話清單,目前只放入使用者剛輸入的一則 user 訊息。client.messages.create() 回傳 response 後,response_text() 會從中取出文字:
def response_text(response) -> str:
return "".join(block.text for block in response.content if block.type == "text")
執行程式並輸入問題:
uv run chat-single-turn
你:Python 要怎麼在 list 裡面新增東西?
AI:可以使用 append() 方法,例如:fruits.append("蘋果")。
這樣就完成一個能接收終端機輸入、呼叫 Claude 並顯示回答的單次問答程式。每次執行只會處理一個獨立問題;接著把它擴展成能持續對話並記住前文的程式。
Messages API 本身是無狀態(Stateless)的,每次呼叫 client.messages.create() 都是一次獨立的請求,模型不會自動取得先前的問答。要讓模型接續前面的話題,應用程式必須保存對話歷史,並在每次呼叫 API 時將完整紀錄一併傳入。
在 src/chat_model_basics/ 新增 with_history.py。程式在迴圈外宣告 messages 列表,並在每次對話時累積內容:
def main() -> None:
client = create_client()
model = get_model_name()
# 在迴圈外建立對話歷史,後續每一輪都會沿用
messages: list[MessageParam] = []
print("輸入 /exit 結束對話。")
# 持續接收問題直到使用者輸入 /exit
while True:
try:
question = input("你:").strip()
except EOFError:
print()
break
if question == "/exit":
break
if not question:
continue
# 傳入既有歷史與新問題,取得加入本輪問答後的歷史
messages = run_turn(client, model, messages, question)
# 最後一筆訊息是模型在本輪產生的回答
print(f"AI:{messages[-1]['content']}")
messages 建立在迴圈外,因此每次執行 run_turn() 時都會沿用同一份對話歷史。若使用者輸入 /exit,程式才會離開迴圈。
run_turn() 負責處理一輪對話。它先將使用者的新問題加入 messages,把完整的歷史交給模型,再將模型的回答也加入同一個列表:
def run_turn(
client,
model: str,
messages: list[MessageParam],
question: str,
) -> list[MessageParam]:
# 將使用者問題加入歷史
messages.append({"role": "user", "content": question})
response = client.messages.create(
model=model,
max_tokens=1024,
system=SYSTEM_PROMPT,
messages=messages,
)
# 將 AI 回答也加入歷史供下一輪使用
messages.append({"role": "assistant", "content": response_text(response)})
return messages
每輪都要把使用者問題與 AI 回答一起放進 messages。如果只保存問題,下一輪模型就不知道自己先前回答了什麼;兩種訊息都保留下來,下一次呼叫 API 時才能傳入完整對話。
執行具有對話歷史的程式:
uv run chat-with-history
你:Python 要怎麼在 list 裡面新增東西?
AI:可以使用 append() 方法,例如:fruits.append("蘋果")。
你:那要怎麼把它刪除?
AI:可以使用 remove("蘋果") 刪除指定元素,或是用 pop() 依索引刪除。
你:/exit
能接續話題後,下一步是為模型設定全域的行為規範。例如:要求使用繁體中文、僅依據已知事實回答,以及在資訊不足時主動向使用者確認,而非自行猜測。
這些系統層級的規則不應該與使用者的聊天內容混在同一個 messages 清單中。透過 API 獨立提供的 system 參數(System Prompt),程式可以在每一輪對話中,為模型提供穩定且持續生效的指導原則。
在 with_history.py 中加入 SYSTEM_PROMPT:
SYSTEM_PROMPT = """你是一個使用繁體中文回答的聊天助理。
回答時使用對話中已經提供的資訊;缺少必要資訊時先提問,不要自行猜測。
"""
呼叫 API 時,透過 system 參數傳入這段固定規則:
response = client.messages.create(
model=model,
max_tokens=1024,
system=SYSTEM_PROMPT,
messages=messages,
)
SYSTEM_PROMPT 不會隨著對話輪次改變。每次呼叫模型時都會傳入相同內容,讓模型在整段對話中遵守一致的語言與回答規則。
加入 System Prompt 之後,程式在每次呼叫模型時,都會同時送出兩份資料:固定的行為規則(system)與累積的對話紀錄(messages)。
這些在單次呼叫中提供給模型的完整輸入內容,在技術上統稱為 Context(上下文)。
為了具體看清 Context 的內容,以剛才的對話為例,當使用者問到第二輪時,傳給模型的 messages 實際上包含了三個項目:
[
{
"role": "user",
"content": "Python 要怎麼在 list 裡面新增東西?",
},
{
"role": "assistant",
"content": '可以使用 append() 方法,例如:fruits.append("蘋果")。',
},
{
"role": "user",
"content": "那要怎麼把它刪除?",
},
]
此時模型收到的 Context 便由兩部分各司其職:
SYSTEM_PROMPT 是系統層級的行為準則與邊界:負責規範角色身分、要求僅能依據已知事實回答,並明定在資訊不足時禁止自行臆測、必須主動提問。messages 是動態的事實脈絡與對話歷程:記錄了先前使用者與模型的互動事實,讓模型知道前面討論的是 Python 的 list 操作,進而能理解第二個問題中的「它」是在詢問如何刪除 list 中的元素。Context 決定了模型在這一輪能使用哪些資訊。缺少先前的問答時,模型無法理解依賴前文的問題;加入對話歷史後,模型才能沿用已經提供的資訊繼續回答。因此,應用程式如何選擇與組裝 Context,會直接影響回答是否連貫且符合需求。
目前程式送給模型的 Context,僅由最基礎的 System Prompt 與對話歷史組成。在後續的系列文章中,我們還會逐步讓 Context 擴展到更多維度——包含 Tool 執行結果、Graph State 任務進度、RAG 檢索出的文件片段,以及長期記憶中的使用者偏好。
但無論未來加入多少資料,核心本質都相同:只有在當次呼叫中實際送進模型的內容,才會成為它當輪能看見的 Context。 應用程式必須根據當前任務挑選必要資料,並確認資料的可信度、時效與存取權限。
將更多資料放進 Context 可以提供更完整的資訊,但每次呼叫模型時都要重新傳送這些內容,也會帶來兩項實際的工程限制:
有了 Context 與 System Prompt,程式已經能記住上下文並遵守角色規範。但它依然受限於語言模型的本質:無法取得即時資料,也無法操作外部系統。
例如,即使模型能透過對話歷史(Context)記住使用者人在台中、打算出門散步,但當使用者接著問:「那現在氣溫幾度、會不會下雨?」時,模型依然缺乏當下的即時氣象資料,不能也不應該憑空捏造溫度與降雨機率。
要解決這個問題,應用程式必須提供一個查詢即時資料的 Python 函式,並授權模型在需要時主動提出調用請求,也就是 Tool(工具調用 / Function Calling)。
下一篇,我們將為程式接入第一個即時天氣查詢 Tool,讓對話程式具備取得外部即時資料的能力。